/****

    activequant - activestocks.eu

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

	
	contact  : contact@activestocks.eu
    homepage : http://www.activestocks.eu

****/
package org.activequant.dao.hibernate;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.activequant.core.domainmodel.InstrumentSpecification;
import org.activequant.core.domainmodel.SeriesSpecification;
import org.activequant.core.domainmodel.data.Candle;
import org.activequant.core.domainmodel.data.MarketDataEntity;
import org.activequant.core.types.TimeStamp;
import org.activequant.dao.IMarketDataEntityDao;
import org.activequant.math.algorithms.Interval;
import org.activequant.util.exceptions.DaoException;
import org.activequant.util.tools.ArrayUtils;
import org.apache.log4j.Logger;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Expression;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.springframework.orm.hibernate3.HibernateCallback;


/**
 * An abstract class. MarketDataEntityBase&lt;T extends MarketDataEntity&lt;T&gt;&gt; extends DaoSupportBase&lt;T&gt; implements IMarketDataEntityDao&lt;T&gt;.<br/>
 * Holds the following associated variables:
 * <ul>
 * <li>clazz(Class&lt;T&gt;)</li>
 * <li>hibernateTemplate(HibernateTemplate)</li>
 * </ul>
 * <br>
 * <b>History:</b><br> 
 *  - [28.09.2007] Created new version according to updated domain model (Erik Nijkamp)<br>
 *  - [28.09.2007] Added incremental update functionality (Erik Nijkamp)<br>
 *  - [18.10.2007] Adding logging, fixing date filtering (Ulrich Staudinger)<br>
 *  - [23.10.2007] Renamed + upgraded findBySeriesSpecification() (Erik Nijkamp)<br>
 *  - [24.10.2007] Moved overloaded update method to LookupDecorator (Erik Nijkamp)<br>
 *  - [19.11.2007] DeleteAll() speedup using bulk-delete (Erik Nijkamp)<br>
 *  - [05.01.2008] interval finder added, based on code by Henry Canterburry (Mike Kroutikov)<br>
 *
 *  @author Erik Nijkamp
 *  @author Ulrich Staudinger
 */
abstract class MarketDataEntityBase<T extends MarketDataEntity<T>> extends DaoSupportBase<T> implements IMarketDataEntityDao<T> {

	Logger log = Logger.getLogger(MarketDataEntityBase.class);
	
	/**
	 * Constructs a MarketDataEntityBase&lt;T extends MarketDataEntity&lt;T&gt;&gt;(extends DaoSupportBase&lt;T&gt; implements IMarketDataEntityDao&lt;T&gt;) using the given clazz(Class&lt;T&gt;)
	 * to set its inherited clazz(Class&lt;T&gt;).
	 * @param clazz
	 */
	protected MarketDataEntityBase(Class<T> clazz) {
		super(clazz);
	}
	/**
	 * Constructs a MarketDataEntityBase&lt;T extends MarketDataEntity&lt;T&gt;&gt;(extends DaoSupportBase&lt;T&gt; implements IMarketDataEntityDao&lt;T&gt;) using the given clazz(Class&lt;T&gt;)
	 * to set its inherited clazz(Class&lt;T&gt;).<br/>
	 * The given sessionFactory(SessionFactory) is used to set its Hibernate SessionFactory and create/initialize the inherited hibernateTemplate(HibernateTemplate) for it.
	 * @param clazz
	 * @param sessionFactory
	 */
	protected MarketDataEntityBase(Class<T> clazz, SessionFactory sessionFactory) {
		super(clazz, sessionFactory);
	}
	/**
	 * Finds/returns all persistent instances of the inherited clazz(Class&lt;T&gt;) whose instrumentSpecification(InstrumentSpecification) matches the given spec(InstrumentSpecification),
	 * ordered by their timeStamp(TimeStamp) (descending)
	 */
	@SuppressWarnings("unchecked")
	public T[] findByInstrumentSpecification(InstrumentSpecification spec) throws DaoException {
		// get contract instance
		spec = lookupIfNeeded(spec);		
		// find associated candles
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz).add(Expression.eq("instrumentSpecification", spec)).addOrder(Order.desc("timeStamp"));	
		List<T> list = getHibernateTemplate().findByCriteria(criteria);		
		return ArrayUtils.asArray(list, clazz);		
	}
	/**
	 * <strong>1.</strong> If the given spec(InstrumentSpecification) has a non-null id(Long), it just returns the given spec(InstrumentSpecification) without doing anything else.<br/>
	 * <strong>2.</strong> Otherwise, it uses the given spec(InstrumentSpecification) as an example to find and return the InstrumentSpecification.<br/> 
	 * (it must find exactly one, more than one is considered ambiguous and throws an exception)
	 * @param spec
	 * @return
	 */
//	@SuppressWarnings("unchecked")
//	private InstrumentSpecification lookupIfNeeded(InstrumentSpecification spec) {
//		if(spec.hasId()) return spec; // not needed
//		List<InstrumentSpecification> list = getHibernateTemplate().findByExample(spec);
//		if(list.size() == 0) {
//			log.warn("No instrument specification for example found ...");
//			return null; 
//		} else if(list.size() > 1) {
//			throw new IllegalArgumentException("ambiguity: instrumentSpecification matches more than one database entity: " + spec);
//		}
//		spec = list.get(0);
//		log.debug("Found specification by example, id=" + spec.getId());
//		return spec;
//	}
	
	/**
	 * <strong>1.</strong> If the given spec(InstrumentSpecification) has a non-null id(Long), it just returns the given spec(InstrumentSpecification) without doing anything else.<br/>
	 * <strong>2.</strong> Otherwise, it uses the given spec(InstrumentSpecification) as an example to find and return the InstrumentSpecification.<br/> 
	 * (it must find exactly one, more than one is considered ambiguous and throws an exception)
	 * @param spec
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private InstrumentSpecification lookupIfNeeded(final InstrumentSpecification spec) {
		if(spec.hasId()) return spec; // not needed
		
		return (InstrumentSpecification)getHibernateTemplate().execute(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException, SQLException {
				String queryString=SpecificationDao.createInstrumentSpecificationLookupQuery(spec);
				Query query = session.createQuery(queryString);
				List<InstrumentSpecification> list= query.list();
				if(list.size()==0){
					//log.warn("No instrument specification for example found ...");
					return null; 
				}else if(list.size() > 1) {
					throw new IllegalArgumentException("ambiguity: instrumentSpecification matches more than one database entity: " + spec);
				}
				return list.get(0);
			}
		});
	}
	//public final static TimeStampFormat formatter = new TimeStampFormat("yyyy/MM/dd HH:mm:ss.SSS Z");
	/**
	 * Finds/returns all persistent instances of the inherited clazz(Class&lt;T&gt;) matching the given query(SeriesSpecification). That means:<br/>
	 * - Their instrumentSpecification(InstrumentSpecification) matches the instrumentSpecification(InstrumentSpecification) of the given query(SeriesSpecification); and<br/>
	 * - Their timeStamp(TimeStamp) is within the startTimeStamp(TimeStamp) - endTimeStamp(TimeStamp) interval of the given query(SeriesSpecification); and<br/>
	 * -If they are Candle instances, their timeFrame(TimeFrame) matches the timeFrame(TimeFrame) of the given query(SeriesSpecification).<br/>
	 * Results are ordered by their timeStamp(TimeStamp) (descending)
	 */
	@SuppressWarnings("unchecked")
	public T[] findBySeriesSpecification(SeriesSpecification query) throws DaoException {
		
		//log.debug("looking for data by a series specification "+query.toString());
		
		// get contract instance
		InstrumentSpecification spec = query.getInstrumentSpecification();
		spec = lookupIfNeeded(spec);
		
		// find associated candles
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz).add(Expression.eq("instrumentSpecification", spec));
		//log.info("Trading Range:" + formatter.format(query.getStartTimeStamp()) + " end:" + formatter.format(query.getEndTimeStamp()));
		// date selection
		if(query.getStartTimeStamp() != null) {
			criteria.add(Expression.ge("timeStamp", query.getStartTimeStamp()));
		}
		if(query.getEndTimeStamp() != null) {
			criteria.add(Expression.le("timeStamp", query.getEndTimeStamp()));
		}
		if(Candle.class.isAssignableFrom(clazz) && query.getTimeFrame() != null) {
			criteria.add(Expression.eq("timeFrame", query.getTimeFrame()));
		}
		
		// add ordering
		criteria.addOrder(Order.desc("timeStamp"));
		
		// run						
		List<T> list = getHibernateTemplate().findByCriteria(criteria);		
		return ArrayUtils.asArray(list, clazz);	
	}
	/**
	 * Deletes all persistent instances of the inherited clazz(Class&lt;T&gt;)<br/>
	 * (invokes <code>getHibernateTemplate().bulkUpdate("DELETE " + clazz.getName())</code>)
	 */
	@Override
	public void deleteAll() {
		int rows = getHibernateTemplate().bulkUpdate("DELETE " + clazz.getName());
	    log.debug(rows + " rows affected by bulk delete");
	}
	/**
	 * Deletes all persistent instances of the inherited clazz(Class&lt;T&gt;) whose instrumentSpecification(InstrumentSpecification) matches the given spec(InstrumentSpecification)
	 */
	public void deleteByInstrumentSpecification(InstrumentSpecification spec) throws DaoException {
		delete(findByInstrumentSpecification(spec));
	}
	/**
	 * Deletes all persistent instances of the inherited clazz(Class&lt;T&gt;) matching the given query(SeriesSpecification). That means:<br/>
	 * - Their instrumentSpecification(InstrumentSpecification) matches the instrumentSpecification(InstrumentSpecification) of the given query(SeriesSpecification); and<br/>
	 * - Their timeStamp(TimeStamp) is within the startTimeStamp(TimeStamp) - endTimeStamp(TimeStamp) interval of the given query(SeriesSpecification); and<br/>
	 * -If they are Candle instances, their timeFrame(TimeFrame) matches the timeFrame(TimeFrame) of the given query(SeriesSpecification).
	 */
	public void deleteBySeriesSpecification(SeriesSpecification query) throws DaoException {
		delete(findBySeriesSpecification(query));
	}
	/**
	 * Saves or updates the given entity(T). The entity(T extends MarketDataEntity&lt;T&gt;) has an instrumentSpecification(InstrumentSpecification), which is also 
	 * saved if it doesn't already exist.
	 */
	@Override
	public T update(T entity) throws DaoException {
		List<T> list = new ArrayList<T>();
		list.add(entity);
		return update(list).get(0);
	}
	/**
	 * Saves or updates each of the given entities(T...). Each entity(T extends MarketDataEntity&lt;T&gt;) has an instrumentSpecification(InstrumentSpecification), which is also 
	 * saved if it doesn't already exist. Saves/updates are performed in batches of BATCH_SIZE(100) at a time.<br/>
	 * Then it returns the given entities(T...).
	 */
	@Override
	public T [] update(T... entities) throws DaoException {
		return ArrayUtils.asArray(update(ArrayUtils.asList(entities)), clazz);
	}
	/**
	 * Saves or updates each of the given entities(List&lt;T&gt;). Each entity(T extends MarketDataEntity&lt;T&gt;) has an instrumentSpecification(InstrumentSpecification), which is also 
	 * saved if it doesn't already exist. Saves/updates are performed in batches of BATCH_SIZE(100) at a time.<br/>
	 * Then it returns the given entities(List&lt;T&gt;).
	 */
	@SuppressWarnings("unchecked")
	@Override 
	public List<T> update(final List<T> entities) throws DaoException {
		return (List<T>) getHibernateTemplate().execute(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException, SQLException {		
				InstrumentSpecification lastSeenSpec = null;
				int count = 0;
				for(T entity : entities) {
					InstrumentSpecification spec = entity.getInstrumentSpecification();
					if(!spec.equals(lastSeenSpec)) {
//						log.debug("looking up " + spec);
						lastSeenSpec = lookupIfNeeded(spec);
						//lastSeenSpec = (InstrumentSpecification) session.createCriteria(spec.getClass()).add(Example.create(spec)).uniqueResult();
						if(lastSeenSpec == null) {
//							log.debug("persisting the spec");
							session.save(spec);
							lastSeenSpec = spec;
						}
					}
					entity.setInstrumentSpecification(lastSeenSpec);
					session.saveOrUpdate(entity);
					if((++count % BATCH_SIZE) == 0) {
//						log.debug("batch update flush at count: " + count);
						session.flush();
						session.clear();
					}
				}
				return entities;
			}
		});
	}
	
	/**
	 * Returns timestamp interval for which database has data available.<br/>
	 * Returns an Interval&lt;TimeStamp&gt; whose from(TimeStamp) and to(TimeStamp) are set with the min and max timeStamp(TimeStamp) of the persistent instances of the inherited clazz(Class&lt;T&gt;) 
	 * matching the given query(SeriesSpecification). That means:<br/>
	 * - Their instrumentSpecification(InstrumentSpecification) matches the instrumentSpecification(InstrumentSpecification) of the given query(SeriesSpecification); and<br/>
	 * -If they are Candle instances, their timeFrame(TimeFrame) matches the timeFrame(TimeFrame) of the given query(SeriesSpecification).
	 * @param seriesSpecs series specification
	 * @return timestamp interval, or null if no data.
	 * @throws DaoException
	 */
	@SuppressWarnings("unchecked")
	public Interval<TimeStamp> findAvailableIntervalBySeriesSpecification(SeriesSpecification query) throws DaoException {
//		log.debug("looking for interval by a series specification " + query);
			
		// get contract instance
		InstrumentSpecification spec = query.getInstrumentSpecification();
		spec = lookupIfNeeded(spec);
			
		// find associated candles
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz).add(Expression.eq("instrumentSpecification", spec));
			
		ProjectionList list = Projections.projectionList();
		list.add(Projections.min("timeStamp"));
		list.add(Projections.max("timeStamp"));
		criteria.setProjection(list);
			
		if(Candle.class.isAssignableFrom(clazz) && query.getTimeFrame() != null) {
			criteria.add(Expression.eq("timeFrame", query.getTimeFrame()));
		}
			
		List<T> results = getHibernateTemplate().findByCriteria(criteria);
			
		// Looks like no candles exist, return null
		if(results.size() < 2)
			return null;
			
		return new Interval<TimeStamp>(results.get(0).getTimeStamp(), results.get(1).getTimeStamp());
			
	}
	
}
